Add LangChain workflow span support and refactor LLM invocation#4449
Add LangChain workflow span support and refactor LLM invocation#4449wrisa wants to merge 15 commits intoopen-telemetry:mainfrom
Conversation
There was a problem hiding this comment.
Pull request overview
Note
Copilot was unable to run its full agentic suite in this review.
Adds workflow-level tracing for LangChain (top-level chain runs) and refactors LLM span creation to use the newer GenAI invocation APIs, including an example and expanded test coverage.
Changes:
- Add workflow invocation (INTERNAL span) start/stop/error handling via
on_chain_*callbacks. - Refactor LLM spans to use
InferenceInvocation(start_inference(),stop(),fail()), and introduce workflow invocation tracking. - Add
opentelemetry-util-genaidependency plus a LangGraph workflow example and new tests.
Reviewed changes
Copilot reviewed 7 out of 8 changed files in this pull request and generated 4 comments.
Show a summary per file
| File | Description |
|---|---|
| instrumentation-genai/opentelemetry-instrumentation-langchain/tests/test_workflow_chain.py | Adds unit tests validating workflow span creation, CSA propagation, and error/no-op paths. |
| instrumentation-genai/opentelemetry-instrumentation-langchain/src/opentelemetry/instrumentation/langchain/invocation_manager.py | Makes invocation state nullable to support runs without an associated GenAI invocation. |
| instrumentation-genai/opentelemetry-instrumentation-langchain/src/opentelemetry/instrumentation/langchain/callback_handler.py | Implements workflow spans for chains and migrates LLM handling to InferenceInvocation. |
| instrumentation-genai/opentelemetry-instrumentation-langchain/pyproject.toml | Updates core instrumentation dependency and adds explicit opentelemetry-util-genai dependency. |
| instrumentation-genai/opentelemetry-instrumentation-langchain/examples/workflow/requirements.txt | Adds dependencies for the new workflow example. |
| instrumentation-genai/opentelemetry-instrumentation-langchain/examples/workflow/main.py | Adds a LangGraph StateGraph workflow example that invokes an LLM node. |
| instrumentation-genai/opentelemetry-instrumentation-langchain/CHANGELOG.md | Notes the new workflow span support/refactor in the changelog. |
| else: | ||
| # TODO: For agent invocation | ||
| self._invocation_manager.add_invocation_state( | ||
| run_id, | ||
| parent_run_id, | ||
| None, # type: ignore[arg-type] | ||
| ) | ||
|
|
There was a problem hiding this comment.
Nested chains are recorded in the invocation manager with invocation=None, but on_chain_end() returns early when the invocation is missing/non-WorkflowInvocation. If parent_run_id is not present in the manager (e.g., out-of-order callbacks or partial instrumentation), this creates orphaned entries that will never be cleaned up. Consider deleting the invocation state on on_chain_end() / on_chain_error() when get_invocation() returns None (or when it’s not a WorkflowInvocation), or avoid storing state at all when the parent is unknown.
| else: | |
| # TODO: For agent invocation | |
| self._invocation_manager.add_invocation_state( | |
| run_id, | |
| parent_run_id, | |
| None, # type: ignore[arg-type] | |
| ) | |
| parent_invocation = self._invocation_manager.get_invocation( | |
| run_id=parent_run_id | |
| ) | |
| if parent_invocation is None or not isinstance( | |
| parent_invocation, WorkflowInvocation | |
| ): | |
| # Do not record nested chain state when the parent is unknown or | |
| # not a workflow; otherwise we can create orphaned entries that | |
| # on_chain_end/on_chain_error cannot clean up. | |
| return | |
| # TODO: For agent invocation | |
| self._invocation_manager.add_invocation_state( | |
| run_id, | |
| parent_run_id, | |
| None, # type: ignore[arg-type] | |
| ) |
| invocation = self._invocation_manager.get_invocation(run_id=run_id) | ||
| if invocation is None or not isinstance( | ||
| invocation, WorkflowInvocation | ||
| ): | ||
| # If the invocation does not exist, we cannot set attributes or end it | ||
| return |
There was a problem hiding this comment.
Nested chains are recorded in the invocation manager with invocation=None, but on_chain_end() returns early when the invocation is missing/non-WorkflowInvocation. If parent_run_id is not present in the manager (e.g., out-of-order callbacks or partial instrumentation), this creates orphaned entries that will never be cleaned up. Consider deleting the invocation state on on_chain_end() / on_chain_error() when get_invocation() returns None (or when it’s not a WorkflowInvocation), or avoid storing state at all when the parent is unknown.
| @dataclass | ||
| class _InvocationState: | ||
| invocation: GenAIInvocation | ||
| invocation: Optional[GenAIInvocation] |
There was a problem hiding this comment.
Now that _InvocationState.invocation is Optional[GenAIInvocation], _InvocationManager.add_invocation_state(...) should accept Optional[GenAIInvocation] as well (and callers should no longer need # type: ignore[arg-type]). Updating the manager’s method signature and any related typing will keep the public surface consistent and prevent type-suppression from hiding real issues.
There was a problem hiding this comment.
If you use copilot's suggestion earlier to just return when you have no invocation, we don't need to set optional here. Either path would work
| top_p=0.9, | ||
| frequency_penalty=0.5, | ||
| presence_penalty=0.5, | ||
| stop_sequences=["\n", "Human:", "AI:"], |
There was a problem hiding this comment.
ChatOpenAI typically expects stop (not stop_sequences) for stop tokens; using an unsupported constructor kwarg will raise at runtime and make the example fail. Update the example to use the correct parameter name(s) supported by langchain_openai.ChatOpenAI for the pinned langchain==0.3.21.
| stop_sequences=["\n", "Human:", "AI:"], | |
| stop=["\n", "Human:", "AI:"], |
| # Uncomment after langchain instrumentation is released | ||
| # opentelemetry-instrumentation-langchain~=2.0b0.dev No newline at end of file |
There was a problem hiding this comment.
can delete this if not needed anymore
| @dataclass | ||
| class _InvocationState: | ||
| invocation: GenAIInvocation | ||
| invocation: Optional[GenAIInvocation] |
There was a problem hiding this comment.
If you use copilot's suggestion earlier to just return when you have no invocation, we don't need to set optional here. Either path would work
Description
opentelemetry-util-genaias an explicit dependency inpyproject.toml.examples/workflow/main.py)See sample workflow and inference spans below,

Fixes # (issue)
Type of change
Please delete options that are not relevant.
How Has This Been Tested?
Please describe the tests that you ran to verify your changes. Provide instructions so we can reproduce. Please also list any relevant details for your test configuration
Does This PR Require a Core Repo Change?
Checklist:
See contributing.md for styleguide, changelog guidelines, and more.